
package com.android.formula.dao;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import com.android.formula.utils.Constants;
import com.android.formula.utils.LogUtils;

/**
 * Define the basic method to work with database: insert, update, delete, get...
 */
public abstract class BaseDao<T> {

    private static final String TAG = "BaseDao";

    private Class<T> tClass;

    /**
     * The application context.
     */
    protected Context mContext;

    /**
     * Keep the static connection to database.
     */
    protected SQLiteDatabase mDB;

    /**
     * default id column
     */
    protected String mIdCol = Constants.DB_COL_ID;

    /**
     * The default constructor.
     */
    public BaseDao(Class<T> tClass, Context context, SQLiteDatabase db) {
        mContext = context;
        mDB = db;
        // if (MyApp.mDatabaseConnection != null) {
        // mDB = MyApp.mDatabaseConnection.getDatabase();
        // }
        this.tClass = tClass;
    }

    public void createTable(SQLiteDatabase db) {
        if (getTableName(tClass) == null) {
            return;
        }
        final String comma = ", ";
        StringBuffer sql = new StringBuffer("CREATE TABLE ");
        StringBuffer primarykeys = new StringBuffer("PRIMARY KEY (");
        sql.append(getTableName(tClass));
        sql.append(" (");

        for (Field field : tClass.getDeclaredFields()) {
            Column fieldEntityAnnotation = field.getAnnotation(Column.class);
            if (fieldEntityAnnotation != null) {
                sql.append(comma);
                sql.append(fieldEntityAnnotation.name());
                sql.append(" ");
                sql.append(fieldEntityAnnotation.type());

                if (fieldEntityAnnotation.isPrimaryKey()) {
                    sql.append(" NOT NULL");
                    primarykeys.append(comma);
                    primarykeys.append(fieldEntityAnnotation.name());
                }
            }
        }
        primarykeys.append(")");
        int firstCommaPos = primarykeys.indexOf(comma);
        if (firstCommaPos > 0) {
            primarykeys.delete(firstCommaPos, firstCommaPos + comma.length() - 1);
            sql.append(comma);
            sql.append(primarykeys);
        } else {
            sql.append(", _id INTEGER PRIMARY KEY AUTOINCREMENT");
        }
        firstCommaPos = sql.indexOf(comma);
        if (firstCommaPos > 0) {
            sql.delete(firstCommaPos, firstCommaPos + comma.length() - 1);
        }
        sql.append(");");
        LogUtils.logError(TAG, sql.toString());
        db.execSQL(sql.toString());
    }

    /**
     * Get record by specified id.
     * 
     * @param id specified id
     * @return the cursor
     */
    public Cursor get(String idCol, String idValue) {
        Cursor cursor = null;
        StringBuffer sql = new StringBuffer();
        sql.append(" select * ");
        sql.append(" from " + getTableName(tClass));
        sql.append(" where ").append(idCol).append(" = ?");
        cursor = mDB.rawQuery(sql.toString(), new String[] {
            idValue
        });
        return cursor;
    }

    public ArrayList<T> getBySpecifiedField(String fieldName, String fieldValue) {
        if (mDB == null) {
            return null;
        }

        ArrayList<T> items = new ArrayList<T>();
        Cursor cursor = null;

        synchronized (mDB) {
            cursor = mDB.query(getTableName(tClass), new String[] {
                fieldName
            }, null, new String[] {
                fieldValue
            }, null, null, null);
        }
        // convert cursor to list items.
        if (cursor != null && cursor.getCount() > 0) {
            for (int i = 0; i < cursor.getCount(); i++) {
                cursor.moveToPosition(i);
                T newTObject;
                try {
                    newTObject = tClass.newInstance();
                    bindObject(newTObject, cursor);
                    items.add(newTObject);
                } catch (InstantiationException e) {
                } catch (IllegalAccessException e) {
                } catch (NoSuchFieldException e) {
                }
            }
            cursor.close();
            cursor = null;
        }
        return items;
    }
    
    public ArrayList<T> getItemsFromId(String colName, String idValue) {
        if (mDB == null) {
            return null;
        }

        ArrayList<T> items = new ArrayList<T>();
        Cursor cursor = null;

        synchronized (mDB) {
            cursor = get(colName, idValue);
        }
        // convert cursor to list items.
        if (cursor != null && cursor.getCount() > 0) {
            for (int i = 0; i < cursor.getCount(); i++) {
                cursor.moveToPosition(i);
                T newTObject;
                try {
                    newTObject = tClass.newInstance();
                    bindObject(newTObject, cursor);
                    items.add(newTObject);
                } catch (InstantiationException e) {
                } catch (IllegalAccessException e) {
                } catch (NoSuchFieldException e) {
                }
            }
            cursor.close();
            cursor = null;
        }
        return items;
    }

    /**
     * Get all data in a specified table.
     * 
     * @return array list keeps all data of table
     */
    public ArrayList<T> getAll() {

        if (mDB == null) {
            return null;
        }

        ArrayList<T> items = new ArrayList<T>();
        Cursor cursor = null;

        synchronized (mDB) {
            cursor = getAllByCursor();
        }
        // convert cursor to list items.
        if (cursor != null && cursor.getCount() > 0) {
            for (int i = 0; i < cursor.getCount(); i++) {
                cursor.moveToPosition(i);
                T newTObject;
                try {
                    newTObject = tClass.newInstance();
                    bindObject(newTObject, cursor);
                    items.add(newTObject);
                } catch (InstantiationException e) {
                } catch (IllegalAccessException e) {
                } catch (NoSuchFieldException e) {
                }
            }
            cursor.close();
            cursor = null;
        }
        return items;
    }

    /**
     * Get all data of specified table and return in a cursor.
     * 
     * @return the cursor keeps all data of table
     */
    public Cursor getAllByCursor() {
        return mDB.query(getTableName(tClass), null, null, null, null, null, null);
    }

    /**
     * Check whether a specified record exist or not
     * 
     * @param id specified id of record wants to check.
     * @return return true if specified data exist.
     */
    public boolean isExistence(final String idCol, final String idValue) {
        Cursor cursor = get(idCol, idValue);
        if (cursor.getCount() > 0) {
            // Close cursor after using
            cursor.close();
            return true;
        }
        cursor.close();
        return false;
    }

    /**
     * Delete specified record by id.
     * 
     * @param id specified id of record wants to delete.
     * @return true if delete successfully
     */
    public void delete(final String id) {
        StringBuffer whereClause = new StringBuffer(getPrimaryKeyColumnName());
        whereClause.append("='");
        whereClause.append(id);
        whereClause.append("'");
        mDB.delete(getTableName(tClass), whereClause.toString(), null);
    }
    
    /**
     * Delete record from column by id.
     * @param column of record wants to delete.
     * @param id of record wants to delete.
     */
    public void deleteFromColumn(String column, String id) {
        StringBuffer whereClause = new StringBuffer();
        whereClause.append(column);
        whereClause.append("='");
        whereClause.append(id);
        whereClause.append("'");
        mDB.delete(getTableName(tClass), whereClause.toString(), null);
    }

    private String getPrimaryKeyColumnName() {
        for (Field field : tClass.getDeclaredFields()) {
            Column fieldEntityAnnotation = field.getAnnotation(Column.class);
            if (fieldEntityAnnotation != null) {
                String columnName = getColumnName(field);
                if (columnName != null) {
                    Column annotationColumn = field.getAnnotation(Column.class);
                    if (annotationColumn.isPrimaryKey()) {
                        return columnName;
                    }
                }
            }
        }
        return "_id";
    }

    /**
     * Insert a object to corresponding table.
     * 
     * @param object data wants to insert
     */
    public long insert(T item) {
        if (item != null) {
            try {
                ContentValues value = getFilledContentValues(item);
                mDB.beginTransaction();
                long id = mDB.insert(getTableName(tClass), null, value);
                mDB.setTransactionSuccessful();
                LogUtils.logInfo(TAG, "[DB] Insert Success");
                return id;
            } catch (Exception e) {
                LogUtils.logError(TAG, "[DB] Insert Fail");
                LogUtils.logError(TAG, Log.getStackTraceString(e));
            } finally {
                mDB.endTransaction();
                mDB.close();
            }
        }
        return -1;
    }

    public long insert(ContentValues value) {
        long id = mDB.insert(getTableName(tClass), null, value);
        return id;
    }

    /**
     * Delete all data of table.
     */
    public final void deleteAll() {
        mDB.delete(getTableName(tClass), null, null);
    }

    /**
     * Update all fields of item
     * 
     * @param item
     * @param whereClause
     * @param whereArg
     */
    public final void update(T item, String whereClause, String[] whereArg) {
        update(convertToContentValues(item), whereClause, whereArg);
    }

    /**
     * Quick update with ID Column
     * 
     * @param item
     * @param idColumn
     * @param idValue
     */
    public void update(T item, String idColumn, String idValue) {
        String[] whereArg = new String[1];
        whereArg[0] = idValue;
        update(convertToContentValues(item), idColumn + "=?", whereArg);
    }

    /**
     * Quick update with ID Column
     * 
     * @param valueItem
     * @param idColumn
     * @param idValue
     */
    public void update(ContentValues valueItem, String idColumn, String idValue) {
        String[] whereArg = new String[1];
        whereArg[0] = idValue;
        update(valueItem, idColumn + "=?", whereArg);
    }

    /**
     * Update values in ContentValues
     * 
     * @param valueItem
     * @param whereClause
     * @param whereArg
     */
    public void update(ContentValues valueItem, String whereClause, String[] whereArg) {
        synchronized (mDB) {
            try {
                mDB.beginTransaction();
                mDB.update(getTableName(tClass), valueItem, whereClause, whereArg);
                mDB.setTransactionSuccessful();
                LogUtils.logError(TAG, "[DB] Update Success: " + valueItem.toString());
            } catch (Exception e) {
                LogUtils.logError(TAG, "[DB] Insert Fail");
                LogUtils.logError(TAG, Log.getStackTraceString(e));
            } finally {
                mDB.endTransaction();
                mDB.close();
            }
        }
    }

    /**
     * Convert data to values for inserting.
     * 
     * @param item data of an item.
     * @return values from item data to insert to table.
     */
    private ContentValues convertToContentValues(T item) {
        ContentValues contentValues = new ContentValues();
        try {
            for (Field field : item.getClass().getDeclaredFields()) {
                Column fieldEntityAnnotation = field.getAnnotation(Column.class);
                if (fieldEntityAnnotation != null) {
                    if (!fieldEntityAnnotation.isAutoincrement()) {
                        putInContentValues(contentValues, field, item);
                    }
                }
            }
        } catch (Exception e) {
            contentValues = null;
        }

        return contentValues;
    }

    /**
     * Get record by specified id.
     * 
     * @param id specified id
     */
    public T getItem(final String idCol, final String idValue) {
        Cursor cursor = get(idCol, idValue);
        // convert cursor to list items.
        if (cursor != null && cursor.getCount() > 0) {
            cursor.moveToFirst();
            T newTObject = null;
            try {
                newTObject = tClass.newInstance();
                bindObject(newTObject, cursor);
            } catch (InstantiationException e) {
            } catch (IllegalAccessException e) {
            } catch (NoSuchFieldException e) {
            }
            cursor.close();
            cursor = null;
            return newTObject;
        }
        return null;
    }

    private void bindObject(T newTObject, Cursor cursor) throws NoSuchFieldException,
            IllegalAccessException {
        for (Field field : tClass.getDeclaredFields()) {
            if (!field.isAccessible())
                field.setAccessible(true); // for private variables
            Column fieldEntityAnnotation = field.getAnnotation(Column.class);
            if (fieldEntityAnnotation != null) {
                field.set(newTObject, getValueFromCursor(cursor, field));
            }
        }
    }

    // Get content from specific types
    private Object getValueFromCursor(Cursor cursor, Field field) throws IllegalAccessException {
        Class<?> fieldType = field.getType();
        Object value = null;
        int columnIndex = cursor.getColumnIndex(getColumnName(field));
        if (fieldType.isAssignableFrom(Long.class) || fieldType.isAssignableFrom(long.class)) {
            value = cursor.getLong(columnIndex);
        } else if (fieldType.isAssignableFrom(String.class)) {
            value = cursor.getString(columnIndex);
        } else if ((fieldType.isAssignableFrom(Integer.class) || fieldType
                .isAssignableFrom(int.class))) {
            value = cursor.getInt(columnIndex);
        } else if ((fieldType.isAssignableFrom(Byte[].class) || fieldType
                .isAssignableFrom(byte[].class))) {
            value = cursor.getBlob(columnIndex);
        } else if ((fieldType.isAssignableFrom(Double.class) || fieldType
                .isAssignableFrom(double.class))) {
            value = cursor.getDouble(columnIndex);
        } else if ((fieldType.isAssignableFrom(Float.class) || fieldType
                .isAssignableFrom(float.class))) {
            value = cursor.getFloat(columnIndex);
        } else if ((fieldType.isAssignableFrom(Short.class) || fieldType
                .isAssignableFrom(short.class))) {
            value = cursor.getShort(columnIndex);
        } else if (fieldType.isAssignableFrom(Byte.class) || fieldType.isAssignableFrom(byte.class)) {
            value = (byte) cursor.getShort(columnIndex);
        } else if (fieldType.isAssignableFrom(Boolean.class)
                || fieldType.isAssignableFrom(boolean.class)) {
            int booleanInteger = cursor.getInt(columnIndex);
            value = booleanInteger == 1;
        }
        return value;
    }

    public void putInContentValues(ContentValues contentValues, Field field, Object object)
            throws IllegalAccessException {
        if (!field.isAccessible())
            field.setAccessible(true); // for private variables
        Object fieldValue = field.get(object);
        String key = getColumnName(field);
        if (fieldValue instanceof Long) {
            contentValues.put(key, Long.valueOf(fieldValue.toString()));
        } else if (fieldValue instanceof String) {
            contentValues.put(key, fieldValue.toString());
        } else if (fieldValue instanceof Integer) {
            contentValues.put(key, Integer.valueOf(fieldValue.toString()));
        } else if (fieldValue instanceof Float) {
            contentValues.put(key, Float.valueOf(fieldValue.toString()));
        } else if (fieldValue instanceof Byte) {
            contentValues.put(key, Byte.valueOf(fieldValue.toString()));
        } else if (fieldValue instanceof Short) {
            contentValues.put(key, Short.valueOf(fieldValue.toString()));
        } else if (fieldValue instanceof Boolean) {
            contentValues.put(key, Boolean.parseBoolean(fieldValue.toString()));
        } else if (fieldValue instanceof Double) {
            contentValues.put(key, Double.valueOf(fieldValue.toString()));
        } else if (fieldValue instanceof Byte[] || fieldValue instanceof byte[]) {
            try {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
                objectOutputStream.writeObject(fieldValue);
                contentValues.put(key, outputStream.toByteArray());
                objectOutputStream.flush();
                objectOutputStream.close();
                outputStream.flush();
                outputStream.close();
            } catch (Exception e) {
            }
        }
    }

    private static String getColumnName(Field field) {
        Column annotationColumn = field.getAnnotation(Column.class);
        String column = null;
        if (annotationColumn != null) {
            if (annotationColumn.name().equals("")) {
                column = field.getName();
            } else {
                column = annotationColumn.name();
            }
        }
        return column;
    }

    private ContentValues getFilledContentValues(Object object) throws IllegalAccessException {
        ContentValues contentValues = new ContentValues();
        for (Field field : object.getClass().getDeclaredFields()) {
            Column fieldEntityAnnotation = field.getAnnotation(Column.class);
            if (fieldEntityAnnotation != null) {
                if (!fieldEntityAnnotation.isAutoincrement()) {
                    putInContentValues(contentValues, field, object);
                }
            }
        }
        return contentValues;
    }

    private String[] getColumns() {
        boolean isHaveAnyKey = false;
        List<String> columnsList = new ArrayList<String>();
        for (Field field : tClass.getDeclaredFields()) {
            Column fieldEntityAnnotation = field.getAnnotation(Column.class);
            if (fieldEntityAnnotation != null) {
                String columnName = getColumnName(field);
                if (columnName != null)
                    columnsList.add(columnName);
                if (fieldEntityAnnotation.isPrimaryKey()) {
                    isHaveAnyKey = true;
                }
            }
        }
        if (!isHaveAnyKey) {
            columnsList.add("_id");
        }
        String[] columnsArray = new String[columnsList.size()];
        return columnsList.toArray(columnsArray);
    }

    @SuppressWarnings("unused")
    private Cursor selectCursorFromTable(SQLiteDatabase db, String selection,
            String[] selectionArgs, String groupBy, String having, String orderBy) {
        try {
            String table = getTableName(tClass);
            String[] columns = getColumns();
            Cursor cursor = db.query(table, columns, selection, selectionArgs, groupBy, having,
                    orderBy);
            cursor.moveToFirst();
            return cursor;
        } catch (Exception e) {
            return null;
        }
    }

    public static String getTableName(Class<?> tClass) {
        Table annotationTable = tClass.getAnnotation(Table.class);
        String table = tClass.getSimpleName();
        if (annotationTable != null) {
            if (!annotationTable.name().equals("")) {
                table = annotationTable.name();
            }
        }
        return table;
    }
}
